// -------------------------------------------------------------------------------------
// GifImageRep.m
// -------------------------------------------------------------------------------------
// Permission is granted to freely redistribute this source code, and to use fragments
// of this code in your own applications if you find them to be useful.  This class,
// along with the source code, come with no warranty of any kind, and the user assumes
// all responsibility for its use.
// -------------------------------------------------------------------------------------
// Note: Much of the source in this object has been a heavy modification of various
//       public domain sources for GIF file conversion programs.
// -------------------------------------------------------------------------------------

#import <objc/objc.h>
#import <appkit/appkit.h>
#import <libc.h>
#import <stdlib.h>
#import <stdio.h>
#import <string.h>
#import <dpsclient/dpsclient.h>
#import <defaults/defaults.h>
#import "GifImageRep.h"

// -------------------------------------------------------------------------------------
// error codes & descriptions
#define		errNONE					0
#define		errTABLE_OVERFLOW		1
#define		errRASTER_SIZE			2
#define		errNO_COLORMAP			3
#define		errINVALID_FILE			4
#define		errBAD_BLOCK			5
#define		errCANT_LOAD			6
#define		errEND_OF_FILE			7
#define		errNO_IMAGE				8
static char *errDesc[] = {
			"none",
			"code table overflow",
			"raster has the wrong size",
			"no colormap present for image",
			"invalid GIF file",
			"illegal GIF block type",
			"unable to load file (unknown reason)",
			"reached premature end-of-file"
			"no image found in file",
			0
};
const char *errorDesc(int x) { return errDesc[x]; }

// -------------------------------------------------------------------------------------
@implementation GifImageRep

// -------------------------------------------------------------------------------------
// internal support methods
// -------------------------------------------------------------------------------------

/* decode a raster image */
// Note: A 'goto'? Yuck! I know their ugly, but I just haven't remove it yet. sorry.
- (int)_readRaster
{
	u_char		*ch, buf[260], *fill;
	u_int		count, datm;
	int			i, err, code, bits, thisCode;
	int			codeTable[4096][2];	// LZW compression code data
	int			stack[4096 * 2], *sp;
	int			_datasize, _codesize, _maxsize;
	int			_clear, _avail, _oldcode, _firstcode;

	/* initialize */
	err        = errNONE;
	fill       = imageRaster;
	_datasize  = getc(imageStream);
	_clear     = 1 << _datasize;
	_codesize  = _datasize + 1;
	_avail     = _clear + 2;
	_maxsize   = _clear * 2;
	_oldcode   = -1;
	_firstcode = -1;
	bits       = 0;
	datm       = 0;
	sp         = stack;

	/* prefill table */
	for (i = 0; i < 4096; i++) {
		codeTable[i][0] = 0;
		codeTable[i][1] = (i < _clear)? i : 0;
	}
  
	/* decode raster */
	for (count = getc(imageStream); count > 0; count = getc(imageStream)) {
  
		/* read block */
		if (fread(buf, 1, count, imageStream) != count) { err = errEND_OF_FILE; break; }
	
		/* process block */
		for (ch = buf; count-- > 0; ch++) {
			datm += (u_int)(*ch) << bits;
			bits += 8;
			while (bits >= _codesize) {
	  
				/* get code */
				code = datm & ((1 << _codesize) - 1);
				datm >>= _codesize;
				bits -= _codesize;

				/* clear table */
				if (code == _clear) {
					_codesize  = _datasize + 1;
					_maxsize   = _clear * 2;
					_avail	 = _clear + 2;
					_oldcode   = -1;
					_firstcode = -1;
					sp         = stack;
					for (i = 0; i < 4096; i++) {
						codeTable[i][0] = 0;
						codeTable[i][1] = (i < _clear)? i : 0;
					}
					continue;
				}
		
				/* end of decompression */
				if (code == _clear + 1) goto exitloop;  /* for non-standard GIF files */

				/* save first code */
				if (_firstcode == -1) {
					_firstcode = _oldcode = code;
					*fill++ = code;
					continue;
				}

				/* save this code */
				thisCode = code;

				/* code check */
				if (code >= _avail) {
					*sp++ = _firstcode;
					code = _oldcode;
				}

				/* place codes on stack */
				while (code >= _clear) {
					*sp++ = codeTable[code][1];
					if (code == codeTable[code][0]) {
						err = errTABLE_OVERFLOW;
						goto exitloop;
					}
					code = codeTable[code][0];
				}
				*sp++ = _firstcode = codeTable[code][1];

				/* save in table */
				code = _avail;
				if (code < 4096) {
					codeTable[code][0] = _oldcode;
					codeTable[code][1] = _firstcode;
					_avail++;
					if ((_avail >= _maxsize) && (_maxsize < 4096)) {
						_maxsize *= 2;
						_codesize++;
					}
				}

				/* save old code */
				_oldcode = thisCode;
		
				/* flush stack */
				while (sp > stack) *fill++ = *--sp;
		
			}
		}
	
	}
  
exitloop:
	if (err == errEND_OF_FILE) {
		err = errNONE;
		NXLogError("GIF premature end of file");
	}
	if (!err && (fill != imageRaster + rasterSize)) err = errRASTER_SIZE;
	return err;

}

/* load image raster */
- (int)_loadImage
{
	u_char		buf[10];
	BOOL		local, interleaved;
	int			err, left;

	/* set file position */
	if (imagePos) fseek(imageStream, imagePos, SEEK_SET);
	else imagePos = ftell(imageStream);
  
	/* read image information */
	fread(buf, 1, 9, imageStream);
	left        = buf[0] + (buf[1] << 8);
	rasterTop   = buf[2] + (buf[3] << 8);
	pixWide     = buf[4] + (buf[5] << 8);
	pixHigh     = buf[6] + (buf[7] << 8);
	local       = (buf[8] & 0x80)? YES : NO;
	interleaved = (buf[8] & 0x40)? YES : NO;
	rasterSize  = (u_long)pixWide * (u_long)pixHigh;

	/* convert color map */
	if (local) {
		colorMapCount = 1 << ((buf[8] & 0x7) + 1);
		fread(colorMap, 3, colorMapCount, imageStream);
	} else
	if (!isGlobalMap) return errNO_COLORMAP;

	/* read image raster */
	if (!imageRaster) memset((imageRaster=(u_char*)malloc(rasterSize)), 0, rasterSize);
	if ((err = [self _readRaster]) && (err != errRASTER_SIZE)) return err;

	/* remove any interleave */
	if (interleaved) {
		int		i, r = 0;
		u_char	*temp = (u_char*)malloc(rasterSize);
		u_short	*tbl = (u_short*)malloc(pixHigh * sizeof(u_short));
		for (i = rasterTop    ; i < (int)(rasterTop + pixHigh); i += 8) tbl[i] = r++;
		for (i = rasterTop + 4; i < (int)(rasterTop + pixHigh); i += 8) tbl[i] = r++;
		for (i = rasterTop + 2; i < (int)(rasterTop + pixHigh); i += 4) tbl[i] = r++;
		for (i = rasterTop + 1; i < (int)(rasterTop + pixHigh); i += 2) tbl[i] = r++;
		for (i = 0, r = rasterTop; r < (int)(rasterTop + pixHigh); i++, r++)
			memcpy(&temp[i * pixWide], &imageRaster[tbl[r] * pixWide], pixWide);
		free(tbl);
		free(imageRaster);
		imageRaster = temp;
		rasterTop = 0;
	}

	/* return successful */
	return errNONE;
  
}

/* read GIF header info */
- (int)_readGIFHeader
{
	u_int	screenWidth, screenHeight;								// screen dimensions
	u_int	background;
	u_char	buf[256];

	/* rewind file pointer */
	rewind(imageStream);
  
	/* check gif signiture type */
	if (![[self class] validImageType:imageStream]) return errINVALID_FILE;
	
	/* read global information */
	fread(buf, 1, 7, imageStream);
	screenWidth  = buf[0] + (buf[1] << 8);							// not used
	screenHeight = buf[2] + (buf[3] << 8);							// not used
	isGlobalMap  = (buf[4] & 0x80)? YES : NO;						// global color map?
	background   = buf[5];											// not used
	aspectRatio  = (buf[6])? ((float)buf[6] + 15.0) / 64.0 : 1.0;	// aspect ratio

	/* load global colormap */
	if (isGlobalMap) {
		colorMapCount = 1 << ((buf[4] & 0x07) + 1);
		fread(colorMap, 3, colorMapCount, imageStream);
	}
  
	return errNONE;
}

/* initialize from file */
- (int)_readImageAtPosition:(long int)overridePos
{
	int	err;

	/* read gif header info */
	if (err = [self _readGIFHeader]) return err;

	/* set file position override */
	if (overridePos) fseek(imageStream, overridePos, SEEK_SET);
  
	/* loop through file */
	for(;;) {
		int err;
		u_char buf[256], ch = getc(imageStream);
		switch (ch) {
			case ',' :
				if (err = [self _loadImage]) return err;
				imageNextPos = ftell(imageStream);
				return errNONE;
			case '!' :	// skip extension
				for (ch=getc(imageStream); ch=getc(imageStream);)
					fread(buf, 1, ch, imageStream);
				break;
			case '\0':
				break;	/* non-standard files */
			case ';' :
				return imageNextPos? errNONE: errNO_IMAGE;
			default  :
				return errBAD_BLOCK;
		}
	}

	/* return any error */
	return errNONE;
  
}

// -------------------------------------------------------------------------------------
// internal init method
// -------------------------------------------------------------------------------------

/* init from file stream */
- _initFromFile:(const char*)filename atPos:(long int)overridePos
{
	NXSize	pSize;
  
	/* generic initialization */
	[self initDrawMethod:@selector(_drawImage:) inObject:self];
  
	/* local var init */
	imageRaster		= (u_char*)nil;
	pixWide  		= 0;
	pixHigh 		= 0;
	rasterSize		= 0L;
	isGlobalMap		= NO;
	colorMapCount	= 0;
	imagePos		= 0L;
	imageNextPos	= 0L;
  
	/* verify stream */
	imageStream = fopen(filename, "r");
	if (!imageStream) { [self free]; return (id)nil; }
  
	/* initialize */
	loadError = [self _readImageAtPosition:overridePos];
	if (loadError == errNO_IMAGE) { [self free]; return (id)nil; }
	if (!loadError && (!pixWide || !pixHigh)) loadError = errCANT_LOAD;
	if (loadError) {
		NXLogError("GIF stream error: %s", errDesc[loadError]);
		[self free];
		return (id)nil;
	}

	/* init size (check aspect ratio) */
	pSize.width  = (float)pixWide;
	pSize.height = (float)pixHigh;
	if (aspectRatio < 1.0) pSize.width  *= 1.0 / aspectRatio; else
	if (aspectRatio > 1.0) pSize.height *= aspectRatio;

	/* init custom image rep */
	[self setSize:&pSize];
	[self setPixelsWide:pixWide];
	[self setPixelsHigh:pixHigh];
	[self setBitsPerSample:8];
	[self setAlpha:NO];
	[self setNumColors:256];

	return self;
}

// -------------------------------------------------------------------------------------
// public advertised methods
// -------------------------------------------------------------------------------------

/* return true if file is gif image (only check file extension) */
+ (BOOL)validImageFile:(const char*)filename
{
	char	*extn = rindex((char*)filename, '.');
	if (!extn) return NO;
	return strcasecmp(extn,".gif") && strcmp(extn,".g")? NO : YES;
}

/* check file type */
+ (BOOL)validImageType:(FILE*)fd
{
	char	buf[16];
	fread(buf, 1, 6, fd);
	return strncasecmp(buf, "GIF87a", 6) && strncasecmp(buf, "GIF89a", 6)? NO : YES;
}

/* read all images into a list */
+ (List*)newListFromFile:(const char*)filename
{
	id			list, _class = self;
	long int	pos;
	
	/* verify file */
	if (![self validImageFile:filename]) return (id)nil;

	/* start loading images */
	list = [[[List alloc] initCount:1] empty];
	for (pos = 0L;;) {
		if (!(self = [[_class alloc] _initFromFile:filename atPos:pos])) break;
		[list addObject:self];
		pos = imageNextPos;
	}
	
	/* return list */
	if (![list count]) { [list free]; list = (id)nil; }
	return list;
	
}

/* init from file stream */
- initFromFile:(const char*)filename
{
	if (![[self class] validImageFile:filename]) { [self free]; return (id)nil; }
	return [self _initFromFile:filename atPos:0L];
}

/* free gif */
- free
{
	if (imageRaster) free(imageRaster);
	if (imageStream) fclose(imageStream);
	return [super free];
}

// -------------------------------------------------------------------------------------
// custom image draw
// -------------------------------------------------------------------------------------

/* draw image */
- _drawImage:mySelf
{
	int				r, c, bpr;
	u_char			*p;
	NXRect			rect = { { 0.0, 0.0 }, { 0.0, 0.0 } };
	const u_char	*bitmap[5] = { 0, 0, 0, 0, 0 };

	/* make sure the image raster is loaded */
	if (loadError || (!imageRaster && (loadError=[self _loadImage]))) return self;

	/* build image bitmap */
	bitmap[0] = p = (u_char*)malloc(3L * rasterSize);
	for (r = rasterTop; r < (int)(rasterTop + pixHigh); r++) {
		u_char *s = &imageRaster[r * pixWide];
		for (c = 0; c < (int)pixWide; c++, p+=3) memcpy(p, colorMap[s[c]], 3);
	}

	/* render bitmap */
	rect.size = size;
	bpr = (7 + pixWide * 8 * 3) / 8;
	NXDrawBitmap(&rect, pixWide, pixHigh, 8, 3, 8*3, bpr, NO, NO, NX_RGBColorSpace, bitmap);

	/* free bitmap storage */
	free((char*)bitmap[0]);

	/* return successful */
	return self;
  
}

@end
